package com.elitedatai.mchtest.manage.service.impl;

import com.dji.sdk.cloudapi.log.*;
import com.dji.sdk.cloudapi.log.api.AbstractLogService;
import com.dji.sdk.cloudapi.storage.StsCredentialsResponse;
import com.dji.sdk.common.HttpResultResponse;
import com.dji.sdk.common.Pagination;
import com.dji.sdk.common.PaginationData;
import com.dji.sdk.common.SDKManager;
import com.dji.sdk.mqtt.MqttReply;
import com.dji.sdk.mqtt.events.EventsDataRequest;
import com.dji.sdk.mqtt.events.TopicEventsRequest;
import com.dji.sdk.mqtt.events.TopicEventsResponse;
import com.dji.sdk.mqtt.services.ServicesReplyData;
import com.dji.sdk.mqtt.services.TopicServicesResponse;
import com.elitedatai.mchtest.component.mqtt.model.EventsReceiver;
import com.elitedatai.mchtest.component.redis.RedisConst;
import com.elitedatai.mchtest.component.redis.RedisOpsUtils;
import com.elitedatai.mchtest.component.websocket.model.BizCodeEnum;
import com.elitedatai.mchtest.component.websocket.service.IWebSocketMessageService;
import com.elitedatai.mchtest.manage.model.dto.*;
import com.elitedatai.mchtest.manage.model.entity.DeviceLogsEntity;
import com.elitedatai.mchtest.manage.model.enums.DeviceLogsStatusEnum;
import com.elitedatai.mchtest.manage.model.enums.UserTypeEnum;
import com.elitedatai.mchtest.manage.model.param.DeviceLogsCreateParam;
import com.elitedatai.mchtest.manage.model.param.DeviceLogsQueryParam;
import com.elitedatai.mchtest.manage.repository.DeviceLogsRepository;
import com.elitedatai.mchtest.manage.service.IDeviceLogsService;
import com.elitedatai.mchtest.manage.service.IDeviceRedisService;
import com.elitedatai.mchtest.manage.service.ILogsFileService;
import com.elitedatai.mchtest.manage.service.ITopologyService;
import com.elitedatai.mchtest.storage.service.IStorageService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.net.URL;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.data.domain.*;


@Service
@Transactional
@Slf4j
public class DeviceLogsServiceImpl extends AbstractLogService implements IDeviceLogsService {

    private static final String LOGS_FILE_SUFFIX = ".tar";

    @Autowired
    private DeviceLogsRepository repository;

    @Autowired
    private ITopologyService topologyService;

    @Autowired
    private ILogsFileService logsFileService;

    @Autowired
    private IStorageService storageService;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private IWebSocketMessageService webSocketMessageService;

    @Autowired
    private IDeviceRedisService deviceRedisService;

    @Autowired
    private AbstractLogService abstractLogService;

    @Override
    public PaginationData<DeviceLogsDTO> getUploadedLogs(String deviceSn, DeviceLogsQueryParam param) {
        Pageable pageable = PageRequest.of(param.getPage().intValue()-1, param.getPageSize().intValue(),Sort.by(Sort.Direction.DESC, "createTime"));

        Page<DeviceLogsEntity> pagination = repository.query(deviceSn,param.getBeginTime(),param.getEndTime(),param.getStatus(),param.getLogsInformation(),pageable);

        List<DeviceLogsDTO> deviceLogsList = pagination.getContent().stream().map(this::entity2Dto).collect(Collectors.toList());

        return new PaginationData<DeviceLogsDTO>(deviceLogsList, new Pagination(pagination.getNumber() + 1, pagination.getSize(), pagination.getTotalElements()));
    }

    @Override
    public HttpResultResponse getRealTimeLogs(String deviceSn, List<LogModuleEnum> domainList) {
        // 检查设备是否在线，如果设备不在线则返回错误响应
        boolean exist = deviceRedisService.checkDeviceOnline(deviceSn);
        if (!exist) {
            return HttpResultResponse.error("Device is offline.");
        }

        // 调用抽象日志服务获取设备实时日志文件列表
        // 通过SDK管理器获取指定设备的SDK实例，并发送文件上传列表请求
        TopicServicesResponse<ServicesReplyData<FileUploadListResponse>> response = abstractLogService
                .fileuploadList(SDKManager.getDeviceSDK(deviceSn), new FileUploadListRequest().setModuleList(domainList));

        // 遍历响应中的文件列表，确保每个文件都有设备序列号
        for (FileUploadListFile file : response.getData().getOutput().getFiles()) {
            // 如果文件的设备序列号为空，则设置为当前设备序列号
            if (file.getDeviceSn().isBlank()) {
                file.setDeviceSn(deviceSn);
            }
        }

        // 返回成功响应，包含获取到的日志文件列表
        return HttpResultResponse.success(response.getData().getOutput());
    }

    /**
     * 插入设备日志记录到数据库
     *
     * @param bid 日志批次ID
     * @param username 用户名
     * @param deviceSn 设备序列号
     * @param param 设备日志创建参数
     * @return 成功时返回日志批次ID，失败时返回空字符串
     */
    public String insertDeviceLogs(String bid, String username, String deviceSn, DeviceLogsCreateParam param) {
        // 构建设备日志实体对象
        DeviceLogsEntity entity = DeviceLogsEntity.builder()
                .deviceSn(deviceSn)                           // 设置设备序列号
                .username(username)                           // 设置用户名
                .happenTime(param.getHappenTime())            // 设置日志发生时间
                .logsInfo(Objects.requireNonNullElse(param.getLogsInformation(), "")) // 设置日志信息，如果为null则使用空字符串
                .logsId(bid)                                  // 设置日志批次ID
                .status(DeviceLogsStatusEnum.UPLOADING.getVal()) // 设置日志状态为"上传中"
                .build();

        // 将设备日志实体保存到数据库，返回保存结果
        boolean insert = repository.save(entity) != null;

        // 如果保存失败，返回空字符串
        if (!insert) {
            return "";
        }

        // 遍历文件列表，将每个文件信息插入到数据库
        for (FileUploadStartFile file : param.getFiles()) {
            // 存储日志文件信息
            insert = logsFileService.insertFile(file, entity.getLogsId());

            // 如果文件信息插入失败，返回空字符串
            if (!insert) {
                return "";
            }
        }

        // 所有数据都成功插入，返回日志批次ID
        return bid;
    }


    @Override
    public HttpResultResponse pushFileUpload(String username, String deviceSn, DeviceLogsCreateParam param) {
        // 获取临时安全凭证(STS)用于文件上传
        StsCredentialsResponse stsCredentials = storageService.getSTSCredentials();
        // 调整凭证过期时间，提前60秒过期以确保安全
        stsCredentials.getCredentials().setExpire(System.currentTimeMillis() + (stsCredentials.getCredentials().getExpire() - 60) * 1000);
        // 创建日志上传凭证DTO对象
        LogsUploadCredentialsDTO credentialsDTO = new LogsUploadCredentialsDTO(stsCredentials);

        // 获取待上传的文件列表
        List<FileUploadStartFile> files = param.getFiles();
        // 为每个文件设置在存储中的唯一对象键（路径+UUID+文件后缀）
        files.forEach(file -> file.setObjectKey(credentialsDTO.getObjectKeyPrefix() + "/" + UUID.randomUUID().toString() + LOGS_FILE_SUFFIX));

        // 设置文件上传参数
        credentialsDTO.setParams(new FileUploadStartParam().setFiles(files));

        // 调用设备SDK开始文件上传流程
        TopicServicesResponse<ServicesReplyData> response = abstractLogService.fileuploadStart(
                SDKManager.getDeviceSDK(deviceSn), new FileUploadStartRequest()
                        .setCredentials(stsCredentials.getCredentials())  // 临时安全凭证
                        .setBucket(stsCredentials.getBucket())            // 存储桶名称
                        .setEndpoint(stsCredentials.getEndpoint())        // 存储服务端点
                        .setFileStoreDir(stsCredentials.getObjectKeyPrefix()) // 文件存储目录
                        .setProvider(stsCredentials.getProvider())        // 云服务提供商
                        .setRegion(stsCredentials.getRegion())            // 区域信息
                        .setParams(new FileUploadStartParam().setFiles(files))); // 文件参数

        // 检查文件上传启动是否成功
        if (!response.getData().getResult().isSuccess()) {
            // 如果失败，返回错误响应
            return HttpResultResponse.error(response.getData().getResult());
        }

        // 将设备日志信息插入数据库
        String id = this.insertDeviceLogs(response.getBid(), username, deviceSn, param);

        // 在Redis中保存日志上传的进度状态，初始状态为空
        RedisOpsUtils.hashSet(RedisConst.LOGS_FILE_PREFIX + deviceSn, id, LogsOutputProgressDTO.builder().logsId(id).build());

        // 返回成功响应
        return HttpResultResponse.success();
    }

    /**
     * 推送修改日志文件状态的请求（取消上传）。
     * @param deviceSn 设备序列号
     * @param param 文件上传更新请求
     * @return 请求结果
     */
    @Override
    public HttpResultResponse pushUpdateFile(String deviceSn, FileUploadUpdateRequest param) {
        // 调用设备SDK的fileuploadUpdate方法，向设备发送文件上传更新请求（取消上传）
        // 该方法用于更新设备的日志文件上传状态或参数
        TopicServicesResponse<ServicesReplyData> response = abstractLogService.fileuploadUpdate(SDKManager.getDeviceSDK(deviceSn), param);

        // 检查设备响应结果是否成功
        if (!response.getData().getResult().isSuccess()) {
            // 如果设备响应失败，返回包含错误信息的HTTP响应
            return HttpResultResponse.error(response.getData().getResult());
        }

        // 如果设备响应成功，返回成功的HTTP响应
        return HttpResultResponse.success();
    }

    @Override
    public void deleteLogs(String deviceSn, String logsId) {
        repository.deleteByLogsIdAndDeviceSn(logsId, deviceSn);
        logsFileService.deleteFileByLogsId(logsId);
    }

    @Override
    public TopicEventsResponse<MqttReply> fileuploadProgress(TopicEventsRequest<EventsDataRequest<FileUploadProgress>> request, MessageHeaders headers) {
        EventsReceiver<LogsOutputProgressDTO> webSocketData = new EventsReceiver<>();
        webSocketData.setBid(request.getBid());
        webSocketData.setSn(request.getGateway());

        Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(request.getGateway());
        if (deviceOpt.isEmpty()) {
            return null;
        }

        DeviceDTO device = deviceOpt.get();
        String key = RedisConst.LOGS_FILE_PREFIX + request.getGateway();

        try {
            FileUploadProgress output = request.getData().getOutput();
            log.info("Logs upload progress: {}", output.toString());

            LogsOutputProgressDTO progress;
            boolean exist = RedisOpsUtils.checkExist(key);
            if (!exist && !output.getStatus().isEnd()) {
                progress = LogsOutputProgressDTO.builder().logsId(request.getBid()).build();
                RedisOpsUtils.hashSet(key, request.getBid(), progress);
            } else if (exist) {
                progress = (LogsOutputProgressDTO) RedisOpsUtils.hashGet(key, request.getBid());
            } else {
                progress = LogsOutputProgressDTO.builder().build();
            }
            progress.setStatus(output.getStatus());

            // If the logs file is empty, delete the cache of this task.
            List<FileUploadProgressFile> fileReceivers = output.getExt().getFiles();
            if (CollectionUtils.isEmpty(fileReceivers)) {
                RedisOpsUtils.del(key);
            }

            // refresh cache.
            List<LogsProgressDTO> fileProgressList = new ArrayList<>();
            fileReceivers.forEach(file -> {
                LogFileProgress logsProgress = file.getProgress();
                if (!StringUtils.hasText(file.getDeviceSn())) {
                    if (LogModuleEnum.DOCK == file.getModule()) {
                        file.setDeviceSn(request.getGateway());
                    } else if (LogModuleEnum.DRONE == file.getModule()) {
                        file.setDeviceSn(device.getChildDeviceSn());
                    }
                }

                fileProgressList.add(LogsProgressDTO.builder()
                        .deviceSn(file.getDeviceSn())
                        .deviceModelDomain(file.getModule().getDomain())
                        .result(logsProgress.getResult())
                        .status(logsProgress.getStatus().getStatus())
                        .uploadRate(logsProgress.getUploadRate())
                        .progress(((logsProgress.getCurrentStep() - 1) * 100 + logsProgress.getProgress()) / logsProgress.getTotalStep())
                        .build());
            });
            progress.setFiles(fileProgressList);
            webSocketData.setOutput(progress);
            RedisOpsUtils.hashSet(RedisConst.LOGS_FILE_PREFIX + request.getGateway(), request.getBid(), progress);
            // Delete the cache at the end of the task.
            if (output.getStatus().isEnd()) {
                RedisOpsUtils.del(key);
                updateLogsStatus(request.getBid(), DeviceLogsStatusEnum.find(output.getStatus()).getVal());

                fileReceivers.forEach(file -> logsFileService.updateFile(request.getBid(), file));
            }
        } catch (NullPointerException e) {
            this.updateLogsStatus(request.getBid(), DeviceLogsStatusEnum.FAILED.getVal());
            RedisOpsUtils.del(key);
        }

        webSocketMessageService.sendBatch(device.getWorkspaceId(), UserTypeEnum.WEB.getVal(),
                BizCodeEnum.FILE_UPLOAD_PROGRESS.getCode(), webSocketData);

        return new TopicEventsResponse<MqttReply>().setData(MqttReply.success());
    }

    @Override
    public void updateLogsStatus(String logsId, Integer value) {

        DeviceLogsEntity entity = repository.findByLogsId(logsId);
        entity.setStatus(value);
        repository.save(entity);
        if (DeviceLogsStatusEnum.DONE.getVal() == value) {
            logsFileService.updateFileUploadStatus(logsId, true);
        }
    }

    @Override
    public URL getLogsFileUrl(String logsId, String fileId) {
        return logsFileService.getLogsFileUrl(logsId, fileId);
    }

    private DeviceLogsDTO entity2Dto(DeviceLogsEntity entity) {
        if (Objects.isNull(entity)) {
            return null;
        }
        String key = RedisConst.LOGS_FILE_PREFIX + entity.getDeviceSn();
        LogsOutputProgressDTO progress = null;
        if (RedisOpsUtils.hashCheck(key, entity.getLogsId())) {
            progress = (LogsOutputProgressDTO) RedisOpsUtils.hashGet(key, entity.getLogsId());
        }

        return DeviceLogsDTO.builder()
                .logsId(entity.getLogsId())
                .createTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getCreateTime()), ZoneId.systemDefault()))
                .happenTime(Objects.isNull(entity.getHappenTime()) ?
                        null : LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getHappenTime()), ZoneId.systemDefault()))
                .status(entity.getStatus())
                .logsInformation(entity.getLogsInfo())
                .userName(entity.getUsername())
                .deviceLogs(LogsFileUploadListDTO.builder().files(logsFileService.getLogsFileByLogsId(entity.getLogsId())).build())
                .logsProgress(Objects.requireNonNullElse(progress, new LogsOutputProgressDTO()).getFiles())
                .deviceTopo(topologyService.getDeviceTopologyByGatewaySn(entity.getDeviceSn()).orElse(null))
                .build();
    }
}
